﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace ExpressionEvaluator.Extended
{
	internal class ExtensionMethodResolver
	{
		// cache for extension types
		private static Dictionary<string, List<MethodInfo>> _ExtensionMethodsCache = new Dictionary<string,List<MethodInfo>>();

		private List<ExtensionInfo> _Extensions;
		public ExtensionMethodResolver(List<ExtensionInfo> extensions)
		{
			_Extensions = extensions;
		}

		private Dictionary<string, List<MethodInfo>> _ExtensionMethods;
		/// <summary>
		/// Gets the extension methods.
		/// </summary>
		/// <value>The extension methods.</value>
		protected Dictionary<string, List<MethodInfo>> ExtensionMethods
		{
			get
			{
				// do lazy load
				if (_ExtensionMethods == null)
				{
					_ExtensionMethods = LoadExtensionMethods(_Extensions);
				}
				return _ExtensionMethods;
			}
		}

		/// <summary>
		/// Gets the extension methods for the type.
		/// </summary>
		/// <param name="typeName">Name of the type.</param>
		/// <returns>List of extension method for specified type name; or null if none found.</returns>
		public List<MethodInfo> GetExtensionMethodsForType(string typeName)
		{
			if (ExtensionMethods.ContainsKey(typeName))
			{
				return ExtensionMethods[typeName];
			}
			return null;
		}

		/// <summary>
		/// Loads the extension methods.
		/// </summary>
		/// <param name="_Extensions">The _ extensions.</param>
		/// <returns></returns>
		private static Dictionary<string, List<MethodInfo>> LoadExtensionMethods(List<ExtensionInfo> extensions)
		{
			List<ExtensionInfo> extensionsToLoad = new List<ExtensionInfo>();  
			// initialize result

			// add one item to dictionary for every type
			foreach (var item in extensions)
			{
				if (!_ExtensionMethodsCache.ContainsKey(item.TypeName))
				{
					_ExtensionMethodsCache.Add(item.TypeName, new List<MethodInfo>());
					extensionsToLoad.Add(item);
				}
			}

			// if something is not cached, then do search
			if (extensionsToLoad.Count > 0)
			{
				// search all assemblies for extension methods 
				foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
				{
                    var query = GetAssemblyMatchingTypes(extensionsToLoad, assembly);
					// group items by extension type name
					foreach (var item in query.GroupBy(method => method.GetParameters()[0].ParameterType.Name))
					{
						_ExtensionMethodsCache[item.Key].AddRange(item);
					}
				}
			}
			Dictionary<string, List<MethodInfo>> result = new Dictionary<string, List<MethodInfo>>(extensions.Count);
			foreach (var item in extensions)
			{
				if (!result.ContainsKey(item.TypeName))
				{
					result.Add(item.TypeName, _ExtensionMethodsCache[item.TypeName]);
				}
			}
			return result;
		}

        /// <summary>
        /// Gets the matching types in assembly.
        /// </summary>
        /// <param name="extensionsToLoad">The extensions to load.</param>
        /// <param name="assembly">The assembly.</param>
        /// <returns>List of matching types in assembly</returns>
        private static IEnumerable<MethodInfo> GetAssemblyMatchingTypes(List<ExtensionInfo> extensionsToLoad, Assembly assembly)
        {
            try
            {
                // choose only methods extensions methods and method with defined type and assembly
                var query = from type in assembly.GetTypes()
                            where type.IsSealed && !type.IsGenericType && !type.IsNested
                            from method in type.GetMethods(BindingFlags.Static
                                | BindingFlags.Public | BindingFlags.NonPublic)
                            where method.IsDefined(typeof(System.Runtime.CompilerServices.ExtensionAttribute), false)
                            where extensionsToLoad.Any(exInfo =>
                            {
                                Type paramType = method.GetParameters()[0].ParameterType;
                                return (paramType.Name == exInfo.TypeName)
                                    && (method.DeclaringType.Namespace == exInfo.NamespaceName);
                            })
                            select method;
                return query;
            }
            // this exception raises Castle.DynamicProxy and thus we should return something
            catch (ReflectionTypeLoadException)
            {
                return new MethodInfo[]{};
            }
        }
	}
}
